// Copyright 2024 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package main

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/google/syzkaller/pkg/ast"
	"github.com/google/syzkaller/pkg/clangtool"
	"github.com/google/syzkaller/pkg/clangtool/tooltest"
	"github.com/google/syzkaller/pkg/compiler"
	"github.com/google/syzkaller/pkg/declextract"
	"github.com/google/syzkaller/pkg/ifaceprobe"
	"github.com/google/syzkaller/pkg/osutil"
)

func TestClangTool(t *testing.T) {
	tooltest.TestClangTool[declextract.Output](t)
}

func TestDeclextract(t *testing.T) {
	tooltest.ForEachTestFile(t, func(t *testing.T, cfg *clangtool.Config, file string) {
		// Created cache file to avoid running the clang tool.
		goldenFile := file + ".json"
		cacheFile := filepath.Join(cfg.KernelObj, filepath.Base(goldenFile))
		if err := os.Symlink(goldenFile, cacheFile); err != nil {
			t.Fatal(err)
		}
		if err := os.Symlink(filepath.Join(cfg.KernelSrc, "manual.txt"),
			filepath.Join(cfg.KernelObj, "manual.txt")); err != nil {
			t.Fatal(err)
		}
		cfg.ToolBin = "this-is-not-supposed-to-run"
		probeInfo := new(ifaceprobe.Info)
		probeFile := filepath.Join(cfg.KernelSrc, filepath.Base(file)+".probe")
		if osutil.IsExist(probeFile) {
			var err error
			probeInfo, err = readProbeResult(probeFile)
			if err != nil {
				t.Fatal(err)
			}
		}
		coverFile := filepath.Join(cfg.KernelSrc, filepath.Base(file)+".cover")
		if !osutil.IsExist(coverFile) {
			coverFile = ""
		}
		autoFile := filepath.Join(cfg.KernelObj, filepath.Base(file)+".txt")
		runcfg := &config{
			autoFile:  autoFile,
			coverFile: coverFile,
			loadProbeInfo: func() (*ifaceprobe.Info, error) {
				return probeInfo, nil
			},
			Config: cfg,
		}
		res, err := run(runcfg)
		if err != nil {
			if *tooltest.FlagUpdate {
				osutil.CopyFile(autoFile, file+".txt")
				osutil.CopyFile(autoFile+".info", file+".info")
			}
			t.Fatal(err)
		}

		// Check that descriptions compile.
		eh, errors := errorHandler()
		full := ast.ParseGlob(filepath.Join(cfg.KernelObj, "*.txt"), eh)
		if full == nil {
			t.Fatalf("failed to parse full descriptions:\n%s", errors)
		}
		constInfo := compiler.ExtractConsts(full, target, eh)
		if constInfo == nil {
			t.Fatalf("failed to compile full descriptions:\n%s", errors)
		}
		// Fabricate consts.
		consts := make(map[string]uint64)
		for _, info := range constInfo {
			for i, c := range info.Consts {
				consts[c.Name] = uint64(i + 1)
			}
		}
		desc := compiler.Compile(full, consts, target, eh)
		if desc == nil {
			t.Fatalf("failed to compile full descriptions:\n%s", errors)
		}

		// Check that generated structs have the same size/align as they had in C.
		// We assume size/align do not depend on const values (which we fabricated).
		for _, typ := range desc.Types {
			info := res.StructInfo[typ.Name()]
			if info == nil {
				continue
			}
			if typ.Size() != uint64(info.Size) || typ.Alignment() != uint64(info.Align) {
				t.Errorf("incorrect generated type %v: size %v/%v align %v/%v",
					typ.Name(), typ.Size(), info.Size, typ.Alignment(), info.Align)
			}
		}

		// TODO: Ensure that none of the syscalls will be disabled by TransitivelyEnabledCalls.

		tooltest.CompareGoldenFile(t, file+".txt", autoFile)
		tooltest.CompareGoldenFile(t, file+".info", autoFile+".info")
	})
}
